Python 高级

魔法方法

repr

Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 _repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 Vecto r object at 0x10e100070>

1
2
3
4
5
6
7
class AAA:
def __repr__(self):
return "AAA"

aaa = AAA()
print(aaa) # AAA
print(repr(aaa)) # AAA

str

__repr____str__ 区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用__repr__ 作为替代

1
2
3
4
5
6
class AAA:
def __repr__(self):
return "这里写什么就返回什么"

aaa = AAA()
print(str(aaa)) #一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用__repr__ 作为替代

bool

bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在__bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则返回 True。

切片

元组拆包

1
2
3
4
5
print(divmod(20, 8))  # (2, 4)

# 定义一个元组
t = (20, 8)
print(divmod(*t)) # (2, 4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a, b, *rest = range(5)
print(f"a: {a}, b: {b}, rest: {rest}") # a: 0, b: 1, rest: [2, 3, 4]

a, b, *rest = range(3)
print(f"a: {a}, b: {b}, rest: {rest}") # a: 0, b: 1, rest: [2]

a, b, *rest = range(2)
print(f"a: {a}, b: {b}, rest: {rest}") # a: 0, b: 1, rest: []

a, *body, c, d = range(5)
print(f"a: {a}, body: {body}, c: {c}, d:{d}") # a: 0, body: [1, 2], c: 3, d:4

*head, b, c, d = range(5)
print(f"head: {head}, b: {b}, c: {c}, d:{d}") # head: [0, 1], b: 2, c: 3, d:4

嵌套元组拆包

1
2
3
4
5
6
7
8
9
users = [
("张三", "男", "20", ("湖北省", "武汉市", "xx区")),
("李四", "男", "34", ("江苏省", "南京市", "xx区"))
]

for name, sex, age, (province, city, area) in users:
print(province, city, area)
# 湖北省 武汉市 xx区
# 江苏省 南京市 xx区

具名元组

创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from collections import namedtuple

users = namedtuple("users", "name sex age position")
u1 = users("李四", "男", "34", ("江苏省", "南京市", "xx区"))
print(u1.name, u1.sex, u1.age, u1.position)
# 李四 男 34 ('江苏省', '南京市', 'xx区')

# 常用属性和方法
print(u1._fields)
# ('name', 'sex', 'age', 'position')

print(u1._asdict())
# OrderedDict([('name', '李四'), ('sex', '男'), ('age', '34'), ('position', ('江苏省', '南京市', 'xx区'))])

print(u1._replace(sex="女"))
# users(name='李四', sex='女', age='34', position=('江苏省', '南京市', 'xx区'))

print(u1.count("淮安"))
# 0

u2 = users._make(("张三", "男", "20", ("湖北省", "武汉市", "xx区")))
print(u2)
# users(name='张三', sex='男', age='20', position=('湖北省', '武汉市', 'xx区'))

切片赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
l = list(range(10))
print(l) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l[2:5] = [20, 30] # l[2:5] 的位置是从2开始的到4的元素(不包含5),共三个,但是后面的列表值只有两个
print(l) # [0, 1, 20, 30, 5, 6, 7, 8, 9]

del l[5:7]
print(l) # [0, 1, 20, 30, 5, 8, 9]

l[3::2] = [11, 22] # 从下标为 3 的元素开始,每隔两个进行赋值(注意只能赋值一次, 不然会出现错误,例如 l[2::2] = [11, 22] 就会出错)。
print(l) # [0, 1, 20, 11, 5, 22, 9]

l[2:5] = 100 # TypeError: can only assign an iterable
# 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。

序列的增量赋值

增量赋值运算符 += 和 *= 后面的概念和理论都是一样的,这里以 += 进行举例。+= 背后的特殊方法是 __iadd__ (用于“就地加法”, 不会创建新对象,还是修改当前对象的值)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__ 。考虑下面这个简单的表达式:

1
>>> a += b

如果 a 实现了__iadd__ 方法,就会调用这个方法。同时对可变序列(例如 list、bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b) 一样。但是如果 a 没有实现 __iadd__ 的话,a += b 这个表达式的效果就变得跟 a = a + b 一样了:首先计算 a +b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现 __iadd__ 这个方法。总体来讲,可变序列一般都实现了 __iadd__ 方法,因此 += 是就地加法(不会创建新对象)。而不可变序列根本就不支持这个操作(会创建新对象)。上面所说的这些关于 += 的概念也适用于 *=,不同的是,后者相对应的是 __imul__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 展示 *= 在可变和不可变序列上的作用:
l = [1, 2, 3]
print(l) # [1, 2, 3]
print(id(l)) # 2439123847944

l *= 2
print(l) # [1, 2, 3, 1, 2, 3] 值变了
print(id(l)) # 2439123847944 ID 没变

print("====================================")

t = (1, 2, 3)
print(t) # (1, 2, 3)
print(id(t)) # 1865362443288

t *= 2
print(t) # (1, 2, 3, 1, 2, 3) 值变了
print(id(t)) # 1865361721288 ID 也变了

一个关于+=的谜题

1
2
3
4
5
6
7
8
9
10
下面代码会发生下面 4 种情况中的哪一种?
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

a. t 变成 (1, 2, [30, 40, 50, 60])。
b. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
c. 以上两个都不是。
d. a 和 b 都是对的。

答案:a,b。被改变成了 (1, 2, [30, 40, 50, 60]),但是也抛出了异常。如果改写成 t[2].extend([50, 60]) 就能避免这个异常。

list.sort 和 sorted

list.sort 方法会就地排序列表,也就是说不会把原列表复制一份(不会创建新的内存空间,使用 id 查看到的内存地址是一致的)。这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表。

在这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。用返回 None 来表示就地改动这个惯例有个弊端,那就是调用者无法将其串联起来。而返回一个新对象的方法(比如说 str 里的所有方法)则正好相反,它们可以串联起来调用,从而形成连贯接口。

与 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数,reverse 和 key。

1
2
3
4
5
6
7
8
9
a = ["aa", "ccc", "ee", "dddd"]
# 按照字母升序进行排序(sorted 会创建新的对象)
print(sorted(a)) # ['aa', 'ccc', 'dddd', 'ee']

# 按照字母降序进行排序(sorted 会创建新的对象)
print(sorted(a, reverse=True)) # ['ee', 'dddd', 'ccc', 'aa']

# 先按照字母降序进行排序,如果长度相同,那么会保持列表中原来的位置,如下面的 aa 和 ee 的位置。
print(sorted(a, reverse=True, key=len)) # ['dddd', 'ccc', 'aa', 'ee']

因为排序算法是稳定的,aa和 ee的长度都是 2,所以它们的相对位置跟在原来的列表里是一样的。

bisect

bisect 是 python 内置模块,用于有序序列的插入和查找

查找

1
2
3
4
5
import bisect

a = [1,4,6,8,12,15,20]
position = bisect.bisect(a,12) # 5
position_left = bisect.bisect_left(a,12) # 4

插入并排序

1
2
3
4
5
import bisect

a = [1,4,6,8,12,15,20]
bisect.insort(a,13)
print(a) # [1, 4, 6, 8, 12, 13, 15, 20]

综合实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import bisect
HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}'
def demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle)
offset = position * ' |'
print(ROW_FMT.format(needle, position, offset))

if __name__ == '__main__':
# bisect_fn = bisect.bisect_left # 位置在左边
bisect_fn = bisect.bisect
print('DEMO:', bisect_fn.__name__)
print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)

array

https://www.cnblogs.com/jlf0103/p/9168093.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from array import array
from random import random

# 创建一个浮点型数组,并取最后一个值
floats = array('d', (random() for i in range(10**7)))
print(floats[-1])

# 将生成的数组数据写入一个二进制文件中
fp = open('floats.bin', 'wb')
floats.tofile(fp)
fp.close()


# 将数据从二进制文件中读取出来
floats2 = array('d')
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()

print(floats[-1] == floats2[-1]) # True

deque

collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections import deque

# 创建一个大小为 10 的双向队列
dq = deque(range(10), maxlen=10)
print(dq) # deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)

# 队列的旋转操作。接受参数 n,当 n 大于 0 时,队列最右边的元素会被移动的到最左边
dq.rotate(3)
print(dq) # deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)

# 队列的旋转操作。接受参数 n,当 n 小于 0 时,队列最左边的元素会被移动的到最右边
dq.rotate(-4)
print(dq) # deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)

# 左边插入元素
dq.appendleft(-1)
print(dq) # deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)

# 右边添加三个元素,因为大小为 10, 所以会挤掉左边的三个元素
dq.extend([10, 11, 12])
print(dq) # deque([3, 4, 5, 6, 7, 8, 9, 10, 11, 12], maxlen=10)

# 左边添加元素(注意这里是倒序)因为大小为 10, 所以会挤掉右边的三个元素
dq.extendleft([-3, -4, -5])
print(dq) # deque([-5, -4, -3, 3, 4, 5, 6, 7, 8, 9], maxlen=10)

字典找不到 key 时返回默认值

defaultdict

1
2
3
4
5
6
7
8
9
10
11
from collections import defaultdict

# 使用的时候需要给 defaultdict 构造函数传递一个可调用的对象
# 这个对象会在调用 __getitem__ 碰到 key 找不到的情况被调用,并让 __getitem__ 返回一个可调用对象产生的值
# 注意 defaultdict 只是在调用 __getitem__ 找不到 key 的情况下才会有效果,也就是 dd["zhangsan"] 这种写法
# 而 dd.get("zhangsan") 这种写法是不会触发调用 __getitem__ 方法的,所以返回值为 None
# 所有这一切背后的功臣其实是特殊方法 __missing__,而实际上这个特性是所有映射类型都可以选择去支持的。
dd = defaultdict(list)

print(dd.get("zhangsan")) # None
print(dd["zhangsan"]) # []

__missing__

__missing__ 方法只会被 __getitem__ 调用(比如在表达式 d[k] 中)。提供 __missing__ 方法对 get 或者__contains__(in 运算符会用到这个方法)这些方法的使用没有影响。这也是我在上一节最后的警告中提到,defaultdict 中的default_factory 只对 __getitem__ 有作用的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class StrKeyDict0(dict):
"""
StrKeyDict0 继承了 dict
如果找不到的键本身就是字符串,那就抛出 KeyError 异常
如果找不到的键不是字符串,那么把它转换成字符串再进行查找

isinstance(key, str) 测试在上面的__missing__ 中是必需的。如果没有这个测试,只要 str(k) 返回的是一个存在的键,那么__missing__
方法是没问题的,不管是字符串键还是非字符串键,它都能正常运行。但是如果 str(k) 不是一个存在的键,代码就会陷入无限递归。这是因为
__missing__ 的最后一行中的 self[str(key)] 会调用 __getitem__,而这个 str(key) 又不存在,于是 __missing__又会被调用。

同样在 __contains__ 方法中,也不能使用 key in my_dict 来检查 key 是否存在,因为那也会导致 __contains__ 被递归调用
"""
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]

def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default

def __contains__(self, key):
return key in self.keys() or str(key) in self.keys()

OrderedDict

这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。

1
2
3
4
5
6
7
8
9
10
11
from collections import OrderedDict

new_dict = OrderedDict()
new_dict["a"] = "a"
new_dict["c"] = "c"
new_dict["d"] = "d"
new_dict["b"] = "b"
print(new_dict) # OrderedDict([('a', 'a'), ('c', 'c'), ('d', 'd'), ('b', 'b')])

print(new_dict.popitem()) # ('b', 'b')
print(new_dict.popitem(last=False)) # ('a', 'a')

ChainMap

https://www.cnblogs.com/mangmangbiluo/p/9882097.html

该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。

不可变字典

标准库里所有的映射类型都是可变的,从 Python 3.3 开始,types 模块中引入了一个封装类名叫MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。

1
2
3
4
5
6
7
8
from types import MappingProxyType

d = {"a": "a", "b": "b", "c":"c"}
proxy_d = MappingProxyType(d)

print(proxy_d) # {'a': 'a', 'b': 'b', 'c': 'c'}
print(proxy_d["a"]) # a
proxy_d["a"] = 1 # TypeError: 'mappingproxy' object does not support item assignment

字典中的散列表

散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。在 dict 的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。Python 中可以用 hash() 方法来做这件事情。从 Python 3.3 开始,str、bytes 和 datetime 对象的散列值计算过程中多了随机的“加盐”这一步。所加盐值是 Python 进程内的一个常量,但是每次启动 Python 解释器都会生成一个不同的盐值。随机盐值的加入是为了防止 DOS 攻击而采取的一种安全措施。

为了获取 my_dict[search_key] 背后的值,Python 首先会调用 hash(search_key) 来计算 search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出 KeyError 异常。若不是空的,则表元里会有一对 found_key:found_value。这时候 Python 会检验 search_key == found_key 是否为真,如果它们相等的话,就会返回 found_value。如果 search_key 和found_key 不匹配的话,这种情况称为散列冲突。发生这种情况是因为,散列表所做的其实是把随机的元素映
射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字的一部分。为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元。若这次找到的表元是空的,则同样抛出 KeyError;若非空,或者键匹配,则返回这个值;或者又发现了散列冲突,则重复以上的步骤。

添加新元素和更新现有键值的操作几乎跟上面一样。只不过对于前者,在发现空表元的时候会放入一个新元素;对于后者,在找到相对应的表元后,原表里的值对象会被替换成新值。另外在插入新值时,Python 可能会按照散列表的拥挤程度来决定是否要重新分配内存为它扩容。如果增加了散列表的大小,那散列值所占的位数和用作索引的位数都会随之增加,这样做的目的是为了减少发生散列冲突的概率。

字典键的次序取决于添加顺序

当往 dict 里添加新键而又发生散列冲突的时候,新键可能会被安排存放到另一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DIAL_CODES = [
(86, 'China'),
(91, 'India'),
(1, 'United States'),
(62, 'Indonesia'),
(55, 'Brazil'),
(92, 'Pakistan'),
(880, 'Bangladesh'),
(234, 'Nigeria'),
(7, 'Russia'),
(81, 'Japan'),
]


d1 = dict(DIAL_CODES)
print('d1:', d1.keys())
print('d1:', d1)

d2 = dict(sorted(DIAL_CODES))
print('d2:', d2.keys())
print('d2:', d2)

d3 = dict(sorted(DIAL_CODES, key=lambda x:x[1]))
print('d3:', d3.keys())
print('d3:', d3)

assert d1 == d2 and d2 == d3 # 三个字典相等,但是 key 的顺序不一致

无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。

保留最后 N 个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import deque

# 新建一个固定大小的队列
q = deque(maxlen=3)
# 添加元素
q.append(1)
q.append(2)
q.append(3)
q.append(4)

print(q) # deque([2, 3, 4], maxlen=3)
print(q.pop()) # 删除结尾的元素
print(q.popleft()) # 删除前面的元素

查找最大或最小的 N 个元素

1
2
3
4
5
6
7
8
import heapq

nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
# 获取最大的前三个元素
print(heapq.nlargest(3, nums)) # [42, 37, 23]

# 获取最小的前三个元素
print(heapq.nsmallest(3, nums)) # [-4, 1, 2]

按照某些规则获取查找最大或最小的 N 个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

portfolio = [
{'name': 'IBM', 'shares': 100, 'price': 91.1},
{'name': 'AAPL', 'shares': 50, 'price': 543.22},
{'name': 'FB', 'shares': 200, 'price': 21.09},
{'name': 'HPQ', 'shares': 35, 'price': 31.75},
{'name': 'YHOO', 'shares': 45, 'price': 16.35},
{'name': 'ACME', 'shares': 75, 'price': 115.65}
]

import heapq

# 根据价格获取最大前三个数据
print(heapq.nlargest(3, portfolio, key=lambda x:x["price"]))
# [{'name': 'AAPL', 'shares': 50, 'price': 543.22}, {'name': 'ACME', 'shares': 75, 'price': 115.65}, {'name': 'IBM', 'shares': 100, 'price': 91.1}]

# 根据价格获取最小前三个数据
print(heapq.nsmallest(3, portfolio, key=lambda x:x["price"]))
# [{'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'FB', 'shares': 200, 'price': 21.09}, {'name': 'HPQ', 'shares': 35, 'price': 31.75}]

将元素排序后,每次返回最小的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
import heapq

nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]

heapq.heapify(nums)

# 每次返回一个最小的元素
print(heapq.heappop(nums))
print(heapq.heappop(nums))
print(heapq.heappop(nums))
print(heapq.heappop(nums))
print(heapq.heappop(nums))
print(heapq.heappop(nums))

实现一个优先级队列

1
2
3
4
5
6
7
8
9
10
11
import heapq

nums = [(1, 1), (-1, 3),(-1, 2), (1, 0)]

heapq.heapify(nums)

# 每次返回一个最小的元素, 按照每个元素的第一个值先排序,若第一个值相同在按照第二个值进行排序
print(heapq.heappop(nums))
print(heapq.heappop(nums))
print(heapq.heappop(nums))
print(heapq.heappop(nums))

根据 heapq 的排序原理(按照元素第一个值排序,第一个值相同在按照第二个),heappop 每次返回一个最小的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import heapq

class PriorityQueue:
def __init__(self):
self._queue = [] # 存储元素
self._index = 0 # 添加顺序

def push(self, item, priority):
# 添加元素
heapq.heappush(self._queue, (-priority, self._index, item))
self._index += 1

def pop(self):
# 弹出元素
return heapq.heappop(self._queue)[-1]

class Item:
def __init__(self, name):
self.name = name

def __repr__(self):
return 'Item({!r})'.format(self.name)

q = PriorityQueue()
q.push(Item('foo'), 1)
q.push(Item('bar'), 5)
q.push(Item('spam'), 4)
q.push(Item('grok'), 1) # 添加两个优先级相同的元素,优先级相同会按照添加顺序弹出

print(q.pop())
print(q.pop())
print(q.pop())
print(q.pop())

由于 push 和 pop 操作时间复杂度为 O(log N),其中 N 是堆的大小,因此就算是 N 很大的时候它们运行速度也依旧很快。 在上面代码中,队列包含了一个 (-priority, index, item) 的元组。 优先级为负数的目的是使得元素按照优先级从高到低排序。 这个跟普通的按优先级从低到高排序的堆排序恰巧相反。index 变量的作用是保证同等优先级元素的正确排序。 通过保存一个不断增加的 index 下标变量,可以确保元素按照它们插入的顺序排序。 而且, index 变量也在相同优先级元素比较的时候起到重要作用。

字典中的键映射多个值

普通实现

1
2
3
4
5
6
7
8
9
10
11
# 字典中的每个 key 映射多个值,值是 list,有顺序,可重复
d = {
'a' : [1, 2, 3],
'b' : [4, 5]
}

# 字典中的每个 key 映射多个值,值是 set,无顺序,不可重复
e = {
'a' : {3, 1, 2, 3, 3},
'b' : {4, 5, 4}
}

defaultdict 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import defaultdict

d = defaultdict(list)
d["a"].append(1)
d["a"].append(2)
d["a"].append(4)
d["a"].append(5)
print(d) # defaultdict(<class 'list'>, {'a': [1, 2, 4, 5]})

s = defaultdict(set)
s["a"].add(1)
s["a"].add(2)
s["a"].add(4)
s["a"].add(4)
print(s) # defaultdict(<class 'set'>, {'a': {1, 2, 4}})

setdefault 实现

1
2
3
4
5
c = {} # 一个普通的字典
c.setdefault('a', []).append(1) #
c.setdefault('a', []).append(2) # 如果 a 存在则直接 append
c.setdefault('b', []).append(4)
print(c) # {'a': [1, 2], 'b': [4]}

有序字典

1
2
3
4
5
6
7
8
9
from collections import OrderedDict

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4

print(d) # OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])

OrderedDict 内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候, 它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。需要注意的是,一个 OrderedDict 的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。

查找两字典的相同点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = {
'x' : 1,
'y' : 2,
'z' : 3
}

b = {
'w' : 10,
'x' : 11,
'y' : 2
}

print(a.keys() & b.keys()) # {'y', 'x'}
print(a.keys() - b.keys()) # {'z'}
print(a.items() & b.items()) # {('y', 2)}

删除序列相同元素并保持顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def dedupe(items, key=None):
seen = set()
for item in items:
val = item if key is None else key(item)
if val not in seen:
yield item
seen.add(val)


a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}]
print(list(dedupe(a, key=lambda d: (d['x'],d['y']))))
# [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]

b = [1, 5, 2, 1, 9, 1, 5, 10]
print(list(dedupe(b)))
# [1, 5, 2, 9, 10]

slice

1
2
3
4
5
items = [0, 1, 2, 3, 4, 5, 6]

name_slice = slice(2, 4) # 创建一个带名字的切片 name_slice
print(items[name_slice]) # 利用命名切片,获取元素
print(items[2:4]) # 和这种方式一样,但是这种方式不直观

序列中出现次数最多的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
words = [
'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
'my', 'eyes', "you're", 'under'
]

from collections import Counter

word_counts = Counter(words)
# 取出前三个出现最多的元素
print(word_counts.most_common(3))
# [('eyes', 8), ('the', 5), ('look', 4)

通过某个关键字排序一个字典列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rows = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]

from operator import itemgetter

# 根据 fname 排序
rows_by_fname = sorted(rows, key=itemgetter("fname"))
print(rows_by_fname)

# 根据 fname lname 排序
rows_by_lfname = sorted(rows, key=itemgetter("fname", "lname"))
print(rows_by_lfname)

# 使用匿名函数替换 itemgetter,但是匿名函数效率没有 itemgetter 高
rows_by_fname2 = sorted(rows, key=lambda r: r['fname'])
rows_by_lfname2 = sorted(rows, key=lambda r: (r['lname'],r['fname']))

itemgetter 同样也适用于 max,min

排序不支持比较的对象

可以借助对象的属性的值来进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User:
def __init__(self, user_id):
self.user_id = user_id

def __repr__(self):
return 'User({})'.format(self.user_id)



users = [User(23), User(3), User(99)]
print(sorted(users, key=lambda x:x.user_id))
# [User(3), User(23), User(99)]

# 使用 attrgetter 替代匿名函数,attrgetter 效率更高些
from operator import attrgetter
print(sorted(users, key=attrgetter("user_id")))
# [User(3), User(23), User(99)]

attrgetter同样也适用于 max,min

通过某个字段将记录分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
rows = [
{'address': '5412 N CLARK', 'date': '07/01/2012'},
{'address': '5148 N CLARK', 'date': '07/02/2012'},
{'address': '5800 E 58TH', 'date': '07/02/2012'},
{'address': '2122 N CLARK', 'date': '07/03/2012'},
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
{'address': '1060 W ADDISON', 'date': '07/02/2012'},
{'address': '4801 N BROADWAY', 'date': '07/01/2012'},
{'address': '1039 W GRANVILLE', 'date': '07/03/2012'},
]

# 按照 date 进行分组排序
from itertools import groupby
from operator import itemgetter

# 这里的排序非常重要,因为 groupby 仅仅检查连续的元素。如果不排序,会和期望结果不一样
rows.sort(key=itemgetter('date'))
for k, v in groupby(rows, key=itemgetter("date")):
print(k)
for x in v:
print(" ", x)

"""
07/01/2012
{'address': '5412 N CLARK', 'date': '07/01/2012'}
{'address': '4801 N BROADWAY', 'date': '07/01/2012'}
07/02/2012
{'address': '5148 N CLARK', 'date': '07/02/2012'}
{'address': '5800 E 58TH', 'date': '07/02/2012'}
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}
{'address': '1060 W ADDISON', 'date': '07/02/2012'}
07/03/2012
{'address': '2122 N CLARK', 'date': '07/03/2012'}
{'address': '1039 W GRANVILLE', 'date': '07/03/2012'}
"""

将相同日期的数据放到同一个 list 当中

1
2
3
4
5
6
# 将相同日期的数据放到同一个 list 当中
from collections import defaultdict
rows_by_date = defaultdict(list)
for row in rows:
rows_by_date[row['date']].append(row)
print(rows_by_date)

过滤序列元素

列表推导式

1
2
3
4
# 使用列表推导式进行元素的过滤,此性能不高,较耗内存
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
print([x for x in mylist if x < 0])
# [-5, -7, -1]

生成器

1
2
3
4
# 生成器
a = (x for x in mylist if x < 0)
print(list(a))
# [-5, -7, -1]

Filter

1
2
3
4
5
# Filter
def less(x):
return True if x < 0 else False
print(list(filter(less, mylist)))
# [-5, -7, -1]

compress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# compress
from itertools import compress
addresses = [
'5412 N CLARK',
'5148 N CLARK',
'5800 E 58TH',
'2122 N CLARK',
'5645 N RAVENSWOOD',
'1060 W ADDISON',
'4801 N BROADWAY',
'1039 W GRANVILLE',
]
# 选择器,值为 True 即不过滤 addresses 中的元素
selector = [True, True, False, False, True]
print(list(compress(addresses, selector)))
# ['5412 N CLARK', '5148 N CLARK', '5645 N RAVENSWOOD']

命名元组

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import namedtuple

User = namedtuple("User", ["name", "age"])

u1 = User("zhangsan", 20)
u2 = User("lisi", 30)
print(u1, u2)
# User(name='zhangsan', age=20) User(name='lisi', age=30)

# 修改 namedtuple 的值只能使用 _replace 方法
u11 = u1._replace(name="wanghwu")
print(u11)
# User(name='wanghwu', age=20)